effective-go chapter12

Embedding

Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by_embedding_types within a struct or interface.

Interface embedding is very simple. We’ve mentioned theio.Readerandio.Writerinterfaces before; here are their definitions.

1
2
3
4
5
6
7
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

Theiopackage also exports several other interfaces that specify objects that can implement several such methods. For instance, there isio.ReadWriter, an interface containing bothReadandWrite. We could specifyio.ReadWriterby listing the two methods explicitly, but it’s easier and more evocative to embed the two interfaces to form the new one, like this:

1
2
3
4
5
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}

This says just what it looks like: AReadWritercan do what aReaderdoes_and_what aWriterdoes; it is a union of the embedded interfaces (which must be disjoint sets of methods). Only interfaces can be embedded within interfaces.

The same basic idea applies to structs, but with more far-reaching implications. Thebufiopackage has two struct types,bufio.Readerandbufio.Writer, each of which of course implements the analogous interfaces from packageio. Andbufioalso implements a buffered reader/writer, which it does by combining a reader and a writer into one struct using embedding: it lists the types within the struct but does not give them field names.

1
2
3
4
5
6
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}

The embedded elements are pointers to structs and of course must be initialized to point to valid structs before they can be used. TheReadWriterstruct could be written as

1
2
3
4
type ReadWriter struct {
reader *Reader
writer *Writer
}

but then to promote the methods of the fields and to satisfy theiointerfaces, we would also need to provide forwarding methods, like this:

1
2
3
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}

By embedding the structs directly, we avoid this bookkeeping. The methods of embedded types come along for free, which means thatbufio.ReadWriternot only has the methods ofbufio.Readerandbufio.Writer, it also satisfies all three interfaces:io.Reader,io.Writer, andio.ReadWriter.

There’s an important way in which embedding differs from subclassing. When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one. In our example, when theReadmethod of abufio.ReadWriteris invoked, it has exactly the same effect as the forwarding method written out above; the receiver is thereaderfield of theReadWriter, not theReadWriteritself.

Embedding can also be a simple convenience. This example shows an embedded field alongside a regular, named field.

1
2
3
4
type Job struct {
Command string
*log.Logger
}

TheJobtype now has theLog,Logfand other methods of*log.Logger. We could have given theLoggera field name, of course, but it’s not necessary to do so. And now, once initialized, we can log to theJob:

1
job.Log("starting now...")

TheLoggeris a regular field of theJobstruct, so we can initialize it in the usual way inside the constructor forJob, like this,

1
2
3
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}

or with a composite literal,

1
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

If we need to refer to an embedded field directly, the type name of the field, ignoring the package qualifier, serves as a field name, as it did in theReadmethod of ourReadWriterstruct. Here, if we needed to access the*log.Loggerof aJobvariablejob, we would writejob.Logger, which would be useful if we wanted to refine the methods ofLogger.

1
2
3
func (job *Job) Logf(format string, args ...interface{}) {
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

Embedding types introduces the problem of name conflicts but the rules to resolve them are simple. First, a field or methodXhides any other itemXin a more deeply nested part of the type. Iflog.Loggercontained a field or method calledCommand, theCommandfield ofJobwould dominate it.

Second, if the same name appears at the same nesting level, it is usually an error; it would be erroneous to embedlog.Loggerif theJobstruct contained another field or method calledLogger. However, if the duplicate name is never mentioned in the program outside the type definition, it is OK. This qualification provides some protection against changes made to types embedded from outside; there is no problem if a field is added that conflicts with another field in another subtype if neither field is ever used.


20200131220947.png

0%